 /**
  * Copyright (C) 2008 Google - Enterprise EMEA SE
  *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */

package com.google.gsa.valve.saml.authz;

import com.google.gsa.AuthorizationProcessImpl;
import com.google.gsa.sessions.nonValidSessionException;
import com.google.gsa.valve.configuration.ValveConfiguration;
import com.google.gsa.valve.configuration.ValveConfigurationException;
import com.google.gsa.valve.configuration.ValveConfigurationInstance;
import com.google.gsa.valve.saml.XmlProcessingException;

import com.google.gsa.valve.utils.ValveUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;

import java.net.InetAddress;

import java.text.SimpleDateFormat;

import java.util.Date;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.soap.SOAPBody;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder;
import org.apache.log4j.Logger;

/**
 * It implements the server process that treats the SAML authorization 
 * requests coming from the appliance. Each request includes the username and 
 * the URL, so that this servlet uses the user information internally stored 
 * in a session in order to authorize the access to that content.
 * <p>
 * It invokes the root authorization process to get the authorization class 
 * that is going to process it. It gets that result and sends it back to the 
 * caller (appliance) in SAML format. If it's a 20X response (OK), it returns a 
 * "Permit" message and if not, it'll return a "Deny".
 * It doesn't care about the content sent from the content source as it only 
 * checks security.
 * <p>
 * In the case the root authorization would not have any URL pattern that 
 * matches with the content URL, it sends a -1 error code that is treated 
 * here as an "Indeterminate" response.
 * 
 * 
 */
public class SAMLAuthZ extends HttpServlet {

    static final String SOAP_ENV_NS = 
        "http://schemas.xmlsoap.org/soap/envelope/";
    static final String SAML_NS = "urn:oasis:names:tc:SAML:2.0:assertion";
    static final String SAMLP_NS = "urn:oasis:names:tc:SAML:2.0:protocol";

    static final String SAML_STATUS_CODE_SUCCESS = 
        "urn:oasis:names:tc:SAML:2.0:status:Success";
    static final String SAML_STATUS_CODE_REQUESTER = 
        "urn:oasis:names:tc:SAML:2.0:status:Requester";
    static final String SAML_STATUS_CODE_RESPONDER = 
        "urn:oasis:names:tc:SAML:2.0:status:Responder";

    static final String SAML_DECISION_PERMIT = "Permit";
    static final String SAML_DECISION_DENY = "Deny";
    static final String SAML_DECISION_INDETERMINATE = "Indeterminate";

    private static Logger logger = Logger.getLogger(SAMLAuthZ.class);

    //Date Format
    private static final SimpleDateFormat dateFormat = 
        new SimpleDateFormat("yyyy'-'MM'-'dd'T'HH:mm:ss'Z'");

    //HTTP Authorization Process
    private String authorizationProcessClsName = null;
    private AuthorizationProcessImpl authorizationProcessCls = null;


    /** 
     * Processes requests for both HTTP <code>GET</code> and <code>POST</code> 
     * methods. It gets the SAML authorization request from the appliance and 
     * processes it accordingly.
     * 
     * @param request HTTP request
     * @param response HTTP response
     */
    protected void processRequest(HttpServletRequest request, 
                                  HttpServletResponse response) throws ServletException, 
                                                                       IOException {
        logger.debug("AuthzDecisionQuery received");

        response.setContentType("text/xml;charset=utf-8");

        //set Authorization class
        try {
            setAuthorizationClass();
        } catch (ValveConfigurationException e) {
            logger.error("Configuration error when setting Authorization instance: " + 
                         e);
        }

        String buildStatus = SAML_STATUS_CODE_SUCCESS;

        AuthzDecisionQuery query = null;

        try {
            query = extractQueryFromRequest(request);
        } catch (XmlProcessingException ex) {
            logger.error("Bad input XML string - will respond " + 
                         SAML_STATUS_CODE_REQUESTER, ex);
            buildStatus = SAML_STATUS_CODE_REQUESTER;
        } catch (Exception ex) {
            logger.error("Bad input XML string - unable to respond", ex);
            throw new ServletException(ex);
        }

        String decisionResult = SAML_DECISION_INDETERMINATE;
        if (query != null) {
            try {
                decisionResult = getDecision(request, response, query);
            } catch (Exception ex) {
                logger.error("Problems getting decision - will respond " + 
                             SAML_STATUS_CODE_RESPONDER, ex);
                buildStatus = SAML_STATUS_CODE_RESPONDER;
            }

        } else {
            query = new AuthzDecisionQuery();
        }

        PrintWriter out = response.getWriter();

        logger.debug("AuthzDecisionQuery received with ID=\"" + query.getId() + 
                     "\"" + " for accessing resource=\"" + 
                     query.getResource() + "\"" + " by subject=\"" + 
                     query.getSubject() + "\"" + " with action=\"" + 
                     query.getAction() + "\"" + " (namespace=\"" + 
                     query.getActionNamespace() + "\")" + 
                     ", responding decision=\"" + decisionResult + "\"");

        try {
            buildResponse(buildStatus, out, query, decisionResult);
        } catch (Exception ex) {
            logger.error("Problems generating SOAP response - unable to respond", 
                         ex);
            throw new ServletException(ex);
        }

        out.close();
    }

    /** 
     * Servlet's doGet
     * 
     * @param request servlet request
     * @param response servlet response
     */
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) throws ServletException, 
                                                              IOException {
        processRequest(request, response);
    }

    /** 
     * Servlet's doPost
     * 
     * @param request servlet request
     * @param response servlet response
     */
    protected void doPost(HttpServletRequest request, 
                          HttpServletResponse response) throws ServletException, 
                                                               IOException {
        processRequest(request, response);
    }

    /** 
     * Returns a short description of the servlet.
     */
    public String getServletInfo() {
        return "SAML authorization servlet";
    }

    /**
     * Extracts the SAML authorization query from the request in order to 
     * process it. It sends the authorization object to be processed through 
     * the authorization.
     * 
     * @param request HTTP request
     * 
     * @return authorization decision query
     * 
     * @throws IOException
     * @throws XMLStreamException
     * @throws XmlProcessingException
     */
    private AuthzDecisionQuery extractQueryFromRequest(HttpServletRequest request) throws IOException, 
                                                                                          XMLStreamException, 
                                                                                          XmlProcessingException {

        InputStream is = request.getInputStream();
        XMLInputFactory xif = XMLInputFactory.newInstance();
        XMLStreamReader reader = xif.createXMLStreamReader(is);
        StAXSOAPModelBuilder builder = new StAXSOAPModelBuilder(reader);

        QName qName = null;

        try {
            qName = new QName(SAMLP_NS, "AuthzDecisionQuery");
            OMElement authzDecisionQuery = 
                builder.getSOAPEnvelope().getBody().getFirstChildWithName(qName);
            checkNotNull(authzDecisionQuery);
            qName = new QName(null, "ID");
            OMAttribute id = authzDecisionQuery.getAttribute(qName);
            checkNotNull(id);
            qName = new QName(null, "Resource");
            OMAttribute resource = authzDecisionQuery.getAttribute(qName);
            checkNotNull(resource);
            qName = new QName(SAML_NS, "Subject");
            OMElement subject = 
                authzDecisionQuery.getFirstChildWithName(qName);
            checkNotNull(subject);
            qName = new QName(SAML_NS, "NameID");
            OMElement nameId = subject.getFirstChildWithName(qName);
            checkNotNull(nameId);
            qName = new QName(SAML_NS, "Action");
            OMElement action = authzDecisionQuery.getFirstChildWithName(qName);
            checkNotNull(action);
            qName = new QName(null, "Namespace");
            OMAttribute actionNamespace = action.getAttribute(qName);
            checkNotNull(actionNamespace);

            AuthzDecisionQuery query = 
                new AuthzDecisionQuery(id.getAttributeValue().trim(), 
                                       resource.getAttributeValue().trim(), 
                                       nameId.getText().trim(), 
                                       action.getText().trim(), 
                                       actionNamespace.getAttributeValue().trim());
            return query;
        } catch (NullPointerException ex) {
            throw new XmlProcessingException(qName + 
                                             " not found while processing SAML AuthzDecisionQuery request");
        } catch (Exception ex) {
            throw new XmlProcessingException(qName + 
                                             " not found while processing SAML AuthzDecisionQuery request", 
                                             ex);
        }

    }

    /**
     * Builds the SAML authorization response to be sent to the caller
     * 
     * @param buildStatus status
     * @param writer servlet writer
     * @param query authorization query
     * @param decisionResult decision result
     * 
     * @throws XMLStreamException
     */
    private void buildResponse(String buildStatus, Writer writer, 
                               AuthzDecisionQuery query, 
                               String decisionResult) throws XMLStreamException {

        // build the SOAP elements
        SOAPFactory factory = OMAbstractFactory.getSOAP12Factory();
        SOAPEnvelope soapEnvelope = factory.createSOAPEnvelope();
        soapEnvelope.addAttribute("xmlns:xsd", 
                                  "http://www.w3.org/2001/XMLSchema", null);
        soapEnvelope.addAttribute("xmlns:xsi", 
                                  "http://www.w3.org/2001/XMLSchema-instance", 
                                  null);
        SOAPBody soapBody = factory.createSOAPBody(soapEnvelope);

        String strCurrentDate = "unknown";
        String issuerName = "unknown";

        try {
            //Get current date
            strCurrentDate = getCurrentDate();

            //Get issuer from Local Host
            InetAddress addr = InetAddress.getLocalHost();
            issuerName = addr.getHostName();

        } catch (Exception ex) {
            buildStatus = SAML_STATUS_CODE_RESPONDER;
            logger.error("Problems building response - will respond " + 
                         buildStatus, ex);
        }

        // build the SAML stuff
        OMNamespace samlp = 
            factory.createOMNamespace("urn:oasis:names:tc:SAML:2.0:protocol", 
                                      "samlp");
        OMElement response = 
            factory.createOMElement("Response", samlp, soapBody);
        OMNamespace saml = 
            response.declareNamespace("urn:oasis:names:tc:SAML:2.0:assertion", 
                                      "saml");

        response.addAttribute("ID", "foo1", null);
        response.addAttribute("Version", "2.0", null);
        response.addAttribute("IssueInstant", strCurrentDate, null);

        OMElement status = factory.createOMElement("Status", samlp, response);
        OMElement statusCode = 
            factory.createOMElement("StatusCode", samlp, status);
        statusCode.addAttribute("Value", buildStatus, null);

        if (buildStatus == SAML_STATUS_CODE_SUCCESS) {
            OMElement assertion = 
                factory.createOMElement("Assertion", saml, response);
            assertion.addAttribute("ID", "foo2", null);
            assertion.addAttribute("Version", "2.0", null);

            assertion.addAttribute("IssueInstant", strCurrentDate, null);

            OMElement issuer = 
                factory.createOMElement("Issuer", saml, assertion);
            issuer.setText(issuerName);

            OMElement subject = 
                factory.createOMElement("Subject", saml, assertion);

            OMElement nameID = 
                factory.createOMElement("NameID", saml, subject);
            nameID.setText(query.getSubject());

            OMElement authzDecisionStatement = 
                factory.createOMElement("AuthzDecisionStatement", saml, 
                                        assertion);
            authzDecisionStatement.addAttribute("Decision", decisionResult, 
                                                null);
            authzDecisionStatement.addAttribute("Resource", 
                                                query.getResource(), null);

            OMElement action = 
                factory.createOMElement("Action", saml, authzDecisionStatement);
            action.addAttribute("Namespace", query.getActionNamespace(), null);
            action.setText(query.getAction());
        }

        soapEnvelope.serializeAndConsume(writer);
    }

    /**
     * Checks the Object is not null
     * 
     * @param object the object instance
     */
    public static final void checkNotNull(final Object object) {
        if (object == null) {
            throw new NullPointerException();
        }
    }

    /**
     * Gets current date as a String
     * 
     * @return the current date
     */
    private String getCurrentDate() {

        String strCurrentDate = null;

        //Get current Date as String
        try {
            long currentTimeMillis = System.currentTimeMillis();
            Date currentDate = new Date(currentTimeMillis);
            strCurrentDate = dateFormat.format(currentDate);
        } catch (Exception e) {
            logger.error("Date error: " + e);
        }

        return strCurrentDate;
    }

    /**
     * Sets the root authorization class that drives the authorization 
     * process
     * 
     * @throws ValveConfigurationException
     */
    public void setAuthorizationClass() throws ValveConfigurationException {
        if (authorizationProcessCls == null) {
            setAuthorizationProcessImpl(getValveConfig().getAuthorizationProcessImpl());
        }
    }

    /**
     * Sets the authorization process instance needed to process the request
     * 
     * @param authorizationProcessClsName the name of the authorization class
     *  
     */
    public void setAuthorizationProcessImpl(String authorizationProcessClsName) throws ValveConfigurationException {

        logger.debug("Setting authorizationProcessClsName: " + 
                     authorizationProcessClsName);

        // Cache value
        this.authorizationProcessClsName = authorizationProcessClsName;

        // Protection
        if ((this.authorizationProcessClsName == null) || 
            (this.authorizationProcessClsName.equals(""))) {

            // Throw Configuration Exception
            throw new ValveConfigurationException("Configuration parameter [authorizationProcessImpl] has not been set correctly");

        }

        try {

            // Instantiate the authorization process class

            authorizationProcessCls = 
                    (AuthorizationProcessImpl)Class.forName(authorizationProcessClsName).newInstance();
            authorizationProcessCls.setValveConfiguration(getValveConfig());

        } catch (InstantiationException ie) {

            // Throw Configuration Exception
            throw new ValveConfigurationException("Configuration parameter [authorizationProcessImpl] has not been set correctly - InstantiationException");


        } catch (IllegalAccessException iae) {

            // Throw Configuration Exception
            throw new ValveConfigurationException("Configuration parameter [authorizationProcessImpl] has not been set correctly - IllegalAccessException");

        } catch (ClassNotFoundException cnfe) {

            // Throw Configuration Exception
            throw new ValveConfigurationException("Configuration parameter [authorizationProcessImpl] has not been set correctly - ClassNotFoundException");

        }

    }

    /**
     * It calls the sendRequest method and sends the SAML 
     * response error depending on that
     * 
     * @param request HTTP request
     * @param response HTTP response
     * @param query authorization query
     * 
     * @return
     */
    protected String getDecision(HttpServletRequest request, 
                                 HttpServletResponse response, 
                                 AuthzDecisionQuery query) {

        logger.debug("SAMLAuthZ: getDecision");

        String decision = SAML_DECISION_INDETERMINATE;
        int statusCode = HttpServletResponse.SC_UNAUTHORIZED;

        try {
            statusCode = 
                    sendRequest(request, response, query.getResource(), query.getSubject());
            //check status
            if (statusCode == -1) {
                //It means there was no repository found that matches with the URL
                //So we have to send back an INDETERMINATE
                decision = SAML_DECISION_INDETERMINATE;
            } else {
                if ((statusCode >= 200) && (statusCode < 300)) {
                    decision = SAML_DECISION_PERMIT;
                } else {
                    if ((statusCode >= 400) && (statusCode < 600)) {
                        decision = SAML_DECISION_DENY;
                    } else {
                        decision = SAML_DECISION_INDETERMINATE;
                    }
                }
            }

        } catch (Exception ex) {
            logger.error("Error getting the AuthZ decision: " + ex);
        }
        return decision;
    }

    /**
     * Calls the authorization class that matches with the URL and sends the 
     * HTTP error code back to the caller
     * 
     * @param request HTTP request
     * @param response HTTP response
     * @param url content url
     * @param userId user id
     * 
     * @return HTTP error code
     */
    protected int sendRequest(HttpServletRequest request, 
                              HttpServletResponse response, String url, 
                              String userId) {

        logger.debug("SAMLAuthZ: sendRequest");

        //Set default value
        int statusCode = HttpServletResponse.SC_UNAUTHORIZED;

        String authCookieDomain = null;
        String authCookiePath = null;

        try {

            authCookieDomain = getValveConfig().getAuthCookieDomain();
            authCookiePath = getValveConfig().getAuthCookiePath();

            //Retrieve cookies
            Cookie[] cookies = 
                constructAuthNCookie(userId, authCookieDomain, authCookiePath);

            if (cookies != null) {
                //Log out the cookies 
                for (int i = 0; i < cookies.length; i++) {
                    logger.trace("BEFORE AuthZ: Request Cookie[" + i + "]: " + 
                                 cookies[i].getName() + " - " + 
                                 cookies[i].getDomain() + " - " + 
                                 cookies[i].getValue());
                }
            } else {
                logger.debug("There is no cookie");
            }

            // Launch the authorization process for this domain
            authorizationProcessCls.setValveConfiguration(getValveConfig());

            //Changing the id to null
            statusCode = 
                    authorizationProcessCls.authorize(request, response, cookies, 
                                                      url, null);

            logger.debug("Response status code is: " + statusCode);


        } catch (nonValidSessionException nvE) {

            logger.debug("Non valid session. Proceeding to logout");

            statusCode = 
                    ValveUtils.logout(request, response, null, getValveConfig());

            logger.debug("Setting the error code to: " + statusCode);


        } catch (Exception e) {

            // Debug
            logger.error("Authorization process raised exception: " + 
                         e.getMessage(), e);

            if (statusCode == 0) {
                statusCode = HttpServletResponse.SC_UNAUTHORIZED;
            }

            logger.debug("Setting the error code to: " + statusCode);

        }

        return statusCode;
    }

    /**
     * Constructs the authentication cookie that simulates the main 
     * Security Framework's cookie
     * 
     * @param userId user id
     * @param authCookieDomain cookie domain
     * @param authCookiePath cookie path
     * 
     * @return authentication cookie
     */
    public Cookie[] constructAuthNCookie(String userId, 
                                         String authCookieDomain, 
                                         String authCookiePath) {

        logger.debug("SAMLAuthZ:constructAuthNCookie");

        Cookie[] cookies = null;
        try {
            Cookie authCookie = null;
            String authCookieName = getValveConfig().getAuthCookieName();
            int authMaxAge = 
                new Integer(getValveConfig().getAuthMaxAge()).intValue();
            authCookie = new Cookie(authCookieName, userId);
            authCookie.setDomain(authCookieDomain);
            authCookie.setPath(authCookiePath);
            authCookie.setMaxAge(authMaxAge);
            logger.debug("AuthN cookie created: " + authCookieName + ":" + 
                         userId);
            //add cookie into the array
            cookies = new Cookie[1];
            cookies[0] = authCookie;

        } catch (Exception e) {
            logger.error("Error creating the authentication cookie" + e);
        }
        return cookies;
    }

    /**
     * Gets the Valve configuration instance
     * 
     * @return valve configuration
     */
    public ValveConfiguration getValveConfig() {

        ValveConfiguration valveConfig = null;                

        try {
            valveConfig = ValveConfigurationInstance.getValveConfig();
        } catch (ValveConfigurationException e) {
            logger.debug("Config file instance is not readable: " + 
                         e.getMessage());                                    
            
            valveConfig = readValveConfig ();
            
        }
        
        

        return valveConfig;
    }
    
    /**
     * Reads the Valve config file in case it doesn't exist. This is used when 
     * the Valve Config instance is not readable
     * 
     * @return valve configuration
     */
    private ValveConfiguration readValveConfig () {
        
        ValveConfiguration valveConfig = null;
        
        try {
            String gsaValveConfigPath = ValveUtils.readValveConfigParameter();
            logger.debug ("Reading config file... Config file located at: " + gsaValveConfigPath);
            
            valveConfig = ValveConfigurationInstance.getValveConfig(gsaValveConfigPath);
        } catch (ValveConfigurationException e) {
            logger.error ("Configuration Exception when reading config file: "+e.getMessage(),e);
        }
        
        return valveConfig;
        
    }
    

    /**
     * Class that holds the authorization decision query to take a security 
     * decision to let the user access the resource
     * 
     */
    private static final class AuthzDecisionQuery {

        /**
         * Holds value of property id.
         */
        private String id;

        /**
         * Holds value of property resource.
         */
        private String resource;

        /**
         * Holds value of property subject.
         */
        private String subject;

        /**
         * Holds value of property action.
         */
        private String action;

        /**
         * Holds value of property actionNamespace.
         */
        private String actionNamespace;


        /**
         * Class constructor - default
         * 
         */
        public AuthzDecisionQuery() {
        }

        /**
         * Class contructor
         * 
         * @param id request id
         * @param resource url
         * @param subject user's subject
         * @param action action 
         * @param actionNamespace action namespace
         */
        public AuthzDecisionQuery(String id, String resource, String subject, 
                                  String action, String actionNamespace) {
            this.id = id;
            this.resource = resource;
            this.subject = subject;
            this.action = action;
            this.actionNamespace = actionNamespace;
        }

        /**
         * Gets the id.
         * @return id.
         */
        public String getId() {
            return this.id;
        }

        /**
         * Sets the id.
         * @param id request id
         */
        public void setId(String id) {
            this.id = id;
        }


        /**
         * Gets the user subject.
         *
         * @return user subject.
         */
        public String getSubject() {
            return this.subject;
        }

        /**
         * Sets the user subject.
         *
         * @param subject user subject
         */
        public void setSubject(String subject) {
            this.subject = subject;
        }

        /**
         * Gets the resource.
         * @return resource.
         */
        public String getResource() {
            return this.resource;
        }

        /**
         * Sets the resource.
         * @param resource url.
         */
        public void setResource(String resource) {
            this.resource = resource;
        }

        /**
         * Gets the action.
         * @return action.
         */
        public String getAction() {
            return this.action;
        }

        /**
         * Sets the action.
         * @param action action.
         */
        public void setAction(String action) {
            this.action = action;
        }

        /**
         * Gets the actionNamespace.
         * @return action namespace.
         */
        public String getActionNamespace() {
            return this.actionNamespace;
        }

        /**
         * Sets the actionNamespace.
         * @param actionNamespace action namespace.
         */
        public void setActionNamespace(String actionNamespace) {
            this.actionNamespace = actionNamespace;
        }

    }

}
